/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.hop.core.util;

import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang.StringUtils;

/**
 * @param <T> type of value
 */
public class KeyValue<T> implements Serializable {

  /** The default true values. */
  public static final List<String> DEFAULT_TRUE_VALUES = List.of("j", "y", "on", "true");

  /** Valid key characters. */
  public static final String VALID_KEY_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_-";

  /** Serial version UID. */
  private static final long serialVersionUID = -6847244072467344205L;

  private final String key;

  private T value; // NOPMD

  /**
   * Constructor. Key will be converted to lower case.
   *
   * @param key key to set.
   * @param value value to set, may be null.
   * @throws IllegalArgumentException if key is invalid.
   */
  public KeyValue(final String key, final T value) throws IllegalArgumentException {
    final String keyToSet = StringUtils.lowerCase(key);
    assertKey(keyToSet);
    this.key = keyToSet;
    this.value = value;
  }

  /**
   * Constructor. Key will be converted to lower case. Value is null.
   *
   * @param key key to set.
   * @throws IllegalArgumentException if key is invalid.
   */
  public KeyValue(final String key) throws IllegalArgumentException {
    this(key, null);
  }

  /**
   * @param lowerKey key to test.
   * @throws IllegalArgumentException if key is invalid.
   */
  public static final void assertKey(final String lowerKey) throws IllegalArgumentException {
    Assert.assertNotEmpty(lowerKey, "Key cannot be null or empty");
    if (!StringUtils.containsOnly(lowerKey, VALID_KEY_CHARS)) {
      throw new IllegalArgumentException(
          "Key contains invalid characters [validKeyCharacters=" + VALID_KEY_CHARS + "]");
    }
    if (lowerKey.charAt(0) == '-') {
      throw new IllegalArgumentException("Key must not start with '-'");
    }
    if (lowerKey.endsWith("-")) {
      throw new IllegalArgumentException("Key must not end with '-'");
    }
    if ("_".equals(lowerKey)) {
      throw new IllegalArgumentException("Key must not be  '_'");
    }
  }

  /**
   * @return the key, never null.
   */
  public String getKey() {
    return this.key;
  }

  /**
   * @return the value
   */
  public T getValue() {
    return this.value;
  }

  /**
   * @param value the value to set
   */
  public void setValue(final T value) {
    this.value = value;
  }

  /**
   * @param newValue value to set.
   * @return this.
   */
  public KeyValue<T> value(final T newValue) {
    this.value = newValue;
    return this;
  }

  /**
   * @return value.
   */
  public T value() {
    return this.value;
  }

  /**
   * @param trueValues string true values, case is ignored.
   * @return boolean value, null if value is null.
   */
  public Boolean booleanValue(final String... trueValues) {
    return this.booleanValue(Arrays.asList(trueValues));
  }

  /**
   * @param trueValues string true values, case is ignored.
   * @return boolean value, null if value is null.
   */
  public Boolean booleanValue(final List<String> trueValues) {
    return this.booleanValue(trueValues, true);
  }

  /**
   * @param trueValues string true values.
   * @param ignoreCase ignore case?
   * @return boolean value, null if value is null.
   */
  public Boolean booleanValue(final List<String> trueValues, final boolean ignoreCase) {
    if (this.value == null) {
      return null;
    }
    if (this.value instanceof Boolean) {
      return (Boolean) this.value;
    }
    final String stringValue = this.stringValue();
    if (ignoreCase) {
      return trueValues.contains(StringUtils.lowerCase(stringValue));
    }
    return trueValues.contains(stringValue);
  }

  /**
   * Uses DEFAULT_TRUE_VALUES, ignore case.
   *
   * @return boolean value or null if value is null.
   */
  public Boolean booleanValue() {
    return this.booleanValue(DEFAULT_TRUE_VALUES, true);
  }

  /**
   * @param defaultValue the default value
   * @return boolean value or default value if value is null.
   */
  public Boolean booleanValue(final Boolean defaultValue) {
    final Boolean returnValue = this.booleanValue();
    if (returnValue == null) {
      return defaultValue;
    }
    return returnValue;
  }

  /**
   * @return string value or null if value is null.
   */
  public String stringValue() {
    if (this.value == null) {
      return null;
    }
    if (this.value instanceof String) {
      return (String) this.value;
    }
    return String.valueOf(this.value);
  }

  /**
   * @param defaultValue the default value.
   * @return string value or default value if value is null.
   */
  public String stringValue(final String defaultValue) {
    final String returnValue = this.stringValue();
    if (returnValue == null) {
      return defaultValue;
    }
    return returnValue;
  }

  /**
   * @param defaultValue the default value.
   * @return string value or default value if value is blank.
   */
  public String stringValueDefaultIfBlank(final String defaultValue) {
    final String returnValue = this.stringValue();
    if (StringUtils.isBlank(returnValue)) {
      return defaultValue;
    }
    return returnValue;
  }

  /**
   * @return integer value or null if value is null.
   * @throws NumberFormatException if string value of value cannot be converted to Integer
   */
  public Integer integerValue() throws NumberFormatException {
    if (this.value == null) {
      return null;
    }
    if (this.value instanceof Integer) {
      return (Integer) this.value;
    }
    return Integer.valueOf(String.valueOf(this.value));
  }

  /**
   * @param defaultValue the default value.
   * @return integer value or default value if value is null or cannot be converted to integer.
   */
  public Integer integerValue(final Integer defaultValue) {
    if (this.value == null) {
      return defaultValue;
    }
    try {
      return this.integerValue();
    } catch (NumberFormatException e) {
      return defaultValue;
    }
  }

  /**
   * @return long value or null if value is null.
   * @throws NumberFormatException if string value of value cannot be converted to Long
   */
  public Long longValue() throws NumberFormatException {
    if (this.value == null) {
      return null;
    }
    if (this.value instanceof Long) {
      return (Long) this.value;
    }
    return Long.valueOf(String.valueOf(this.value));
  }

  /**
   * @param defaultValue the default value.
   * @return long value or default value if value is null or cannot be converted to long.
   */
  public Long longValue(final Long defaultValue) {
    if (this.value == null) {
      return defaultValue;
    }
    try {
      return this.longValue();
    } catch (NumberFormatException e) {
      return defaultValue;
    }
  }

  /**
   * @return double value or null if value is null.
   * @throws NumberFormatException if string value of value cannot be converted to Double
   */
  public Double doubleValue() throws NumberFormatException {
    if (this.value == null) {
      return null;
    }
    if (this.value instanceof Double) {
      return (Double) this.value;
    }
    return Double.valueOf(String.valueOf(this.value));
  }

  /**
   * @param defaultValue the default value.
   * @return double value or default value if value is null or cannot be converted to double.
   */
  public Double doubleValue(final Double defaultValue) {
    if (this.value == null) {
      return defaultValue;
    }
    try {
      return this.doubleValue();
    } catch (NumberFormatException e) {
      return defaultValue;
    }
  }

  /**
   * @return float value or null if value is null.
   * @throws NumberFormatException if string value of value cannot be converted to Float
   */
  public Float floatValue() throws NumberFormatException {
    if (this.value == null) {
      return null;
    }
    if (this.value instanceof Float) {
      return (Float) this.value;
    }
    return Float.valueOf(String.valueOf(this.value));
  }

  /**
   * @param defaultValue the default value.
   * @return float value or default value if value is null or cannot be converted to float.
   */
  public Float floatValue(final Float defaultValue) {
    if (this.value == null) {
      return defaultValue;
    }
    try {
      return this.floatValue();
    } catch (NumberFormatException e) {
      return defaultValue;
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    final StringBuilder builder = new StringBuilder();
    builder.append(KeyValue.class.getSimpleName());
    builder.append('(');
    builder.append(this.key);
    if (this.value == null) {
      builder.append("<null>)");
    } else {
      builder.append("=[");
      builder.append(this.value);
      builder.append("])");
    }
    return builder.toString();
  }
}
